# -*- coding: utf-8 -*-
#
#   Copyright 2009 Andrew Wilkinson <andrewjwilkinson@gmail.com>
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import couchdb
import md5

from parser import parse_sql

min_json = None
max_json = dict(zip(map(chr, range(ord("a"),
                                   ord("z")+1)),
                    range(26)))

class Query(object):
    def __init__(self, sql):
        self.raw_sql = sql
        sql = parse_sql(sql)
        self.columns = sql.columns
        self.db_name = sql.table
        self.where_clause = process_expr(sql.where)

        self.is_ranged = (True in [x.is_ranged for x in self.dynamic_keys])
        self.include_docs = True

    @property
    def md5(self):
        return md5.new(self.raw_sql).hexdigest()

    @property
    def dynamic_keys(self):
        return [c for c in self.where_clause if c.is_dynamic]

    @property
    def design_name(self):
        return "couchql_%s" % (self.md5, )

    @property
    def view_name(self):
        return "_design/" + self.design_name + "/_view/couchql"

    def get_map_js(self):
        static_expr = [c.to_js() for c in self.where_clause if not c.is_dynamic]
        dynamic_expr = [c.variable for c in self.dynamic_keys]
        js = """
function (doc) {
"""
        if len(static_expr) > 0:
            js += "if(%s) {" % (" && ".join(static_expr), )

        if len(dynamic_expr) == 0:
            key = "null"
        else:
            key = "[" + ",".join(dynamic_expr) + "]"

        if self.columns == "*":
            doc = "null"
        else:
            raise NotImplementedError

        js += "emit(%s, %s)" % (key, doc)

        js += """
}
"""

        if len(static_expr) > 0:
            js += "}"

        return js

    def run_query(self, con, params, temporary=False):
        if len(params) != len(self.dynamic_keys):
            return ValueError, "Got %i parameters instead of the expected %i." % (len(params), len(self.dynamic_keys))

        if temporary:
            return self.run_temporary_query(con, params)
        else:
            q = self.run_saved_query(con, params)
            try:
                len(q) # Force query to be evaluated
            except couchdb.ResourceNotFound:
                self.save_view(con)
                return self.run_saved_query(con, params)
            else:
                return q

    def run_saved_query(self, con, params):
        if len(params) == 0:
            return con.view(self.view_name, include_docs=self.include_docs)
        elif not self.is_ranged:
            return con.view(self.view_name, key=list(params), include_docs=self.include_docs)
        else:
            params = zip(self.dynamic_keys, params)
            startkey = [p[0].lower_limit(p[1]) for p in params]
            endkey = [p[0].upper_limit(p[1]) for p in params]
            return con.view(self.view_name, startkey=startkey, endkey=endkey, include_docs=self.include_docs)

    def run_temporary_query(self, con, params):
        if len(params) == 0:
            return con.query(self.get_map_js(), include_docs=self.include_docs)
        elif not self.is_ranged:
            return con.query(self.get_map_js(), key=list(params), include_docs=self.include_docs)
        else:
            params = zip(self.dynamic_keys, params)
            startkey = [p[0].lower_limit(p[1]) for p in params]
            endkey = [p[0].upper_limit(p[1]) for p in params]
            return con.query(self.get_map_js(), startkey=startkey, endkey=endkey, include_docs=self.include_docs)

    def save_view(self, con):
        con["_design/"+self.design_name] = { "views": { "couchql": { "map": self.get_map_js() }}}

class Expr(object):
    def __init__(self, expr):
        assert len(expr) == 3, expr
        assert expr[1] in ["=", "!=", "<=", ">="]

        self.clauses = expr

    @property
    def is_dynamic(self):
        return self.clauses[0] == "%s" or self.clauses[2] == "%s"

    @property
    def is_ranged(self):
        return not (self.clauses[1] == "=" or self.clauses[2] == "!=")

    def lower_limit(self, val):
        if self.clauses[0] == "%s":
            if self.clauses[1] == "=":
                return val
            elif self.clauses[1] == "<=":
                return val
            elif selc.clauses[1] == ">=":
                return min_json
            else:
                raise NotImplementedError
        else:
            if self.clauses[1] == "=":
                return val
            elif self.clauses[1] == "<=":
                return min_json
            elif self.clauses[1] == ">=":
                return val
            else:
                raise NotImplementedError

    def upper_limit(self, val):
        if self.clauses[0] == "%s":
            if self.clauses[1] == "=":
                return val
            elif self.clauses[1] == "<=":
                return max_json
            elif selc.clauses[1] == ">=":
                return val
            else:
                raise NotImplementedError
        else:
            if self.clauses[1] == "=":
                return val
            elif self.clauses[1] == "<=":
                return val
            elif self.clauses[1] == ">=":
                return max_json
            else:
                raise NotImplementedError

    @property
    def variable(self):
        assert self.is_dynamic
        if self.clauses[0] == "%s":
            return """doc["%s"]""" % (self.clauses[2], )
        else:
            return """doc["%s"]""" % (self.clauses[0], )

    def to_js(self):
        if self.clauses[1] == "=":
            op = "=="
        else:
            op = self.clauses[1]
        return """doc["%s"] %s %s""" % (self.clauses[0], op, self.clauses[2])

def process_expr(expr):
    if len(expr) == 0:
        return []
    elif len(expr) == 1:
        return process_expr(expr[0])
    else:
        assert len(expr) == 3, expr

        if expr[1] == "and":
            e = process_expr(expr[0])
            e.extend(process_expr(expr[2]))
            return e
        else:
            return [Expr(expr)]
